#include "xlcomm/net/detail/connector.h"

#include "xlcomm/net/detail/sockets_ops.h"

namespace xlcomm {
namespace net {

Connector::Connector(EventLoop* loop, const InetAddress& server_addr)
    : loop_(loop),
      server_addr_(server_addr),
      state_(kDisconnected),
      will_connect_(false),
      retry_delay_ms_(kInitRetryDelayMs) {}

Connector::~Connector() { XLCOMM_ASSERT(!channel_); }

void Connector::Start() {
  will_connect_ = true;
  loop_->RunInLoop(std::bind(&Connector::StartInLoop, this));  // FIXME
}

void Connector::Restart() {
  XLCOMM_ASSERT(loop_->IsInLoopThread());
  state_ = kDisconnected;
  retry_delay_ms_ = kInitRetryDelayMs;
  will_connect_ = true;
  StartInLoop();
}

void Connector::Stop() {
  will_connect_ = false;
  loop_->QueueInLoop(std::bind(&Connector::StopInLoop, this));  // FIXME
  // FIXME: cancel timer
}

void Connector::StartInLoop() {
  XLCOMM_ASSERT(loop_->IsInLoopThread());
  XLCOMM_ASSERT(state_ == kDisconnected);
  if (will_connect_) {
    Connect();
  } else {
    LOG_INFO << "Connector::StartInLoop do not connect";
  }
}

void Connector::StopInLoop() {
  XLCOMM_ASSERT(loop_->IsInLoopThread());
  if (state_ == kConnecting) {
    int sockfd = RemoveAndResetChannel();
    CloseAndRetry(sockfd);
  }
}

void Connector::Connect() {
  int sockfd = sockets::CreateNonblockSockOrDie();
  LOG_INFO << "Connector(fd:" << sockfd << ") connecting to "
           << server_addr_.ToIpPort() << "...";
  int ret = ::connect(sockfd, server_addr_.ToSockaddr(), sizeof(sockaddr));
  int saved_errno = (ret == 0) ? 0 : errno;
  switch (saved_errno) {
    case 0:
    case EINPROGRESS:
    case EINTR:
    case EISCONN:
      Connecting(sockfd);
      break;
    case EAGAIN:
    case EADDRINUSE:
    case EADDRNOTAVAIL:
    case ECONNREFUSED:
    case ENETUNREACH:
      CloseAndRetry(sockfd);
      break;
    case EACCES:
    case EPERM:
    case EAFNOSUPPORT:
    case EALREADY:
    case EBADF:
    case EFAULT:
    case ENOTSOCK:
      errno = saved_errno;
      LOG_SYSERROR(connect) << "dest server_addr: " << server_addr_.ToIpPort();
      sockets::close(sockfd);
      break;
    default:
      errno = saved_errno;
      LOG_SYSERROR(connect)
          << "Unexpected error! dest server_addr: " << server_addr_.ToIpPort();
      sockets::close(sockfd);
      // connectErrorCallback_();
      break;
  }
}

void Connector::Connecting(int sockfd) {
  state_ = kConnecting;
  XLCOMM_ASSERT(!channel_);
  channel_.reset(new Channel(loop_, sockfd));
  channel_->set_write_callback(
      std::bind(&Connector::HandleWrite, this));  // FIXME
  channel_->set_error_callback(
      std::bind(&Connector::HandleError, this));  // FIXME
  channel_->EnableWriting();
}

void Connector::HandleWrite() {
  XLCOMM_ASSERT(loop_->IsInLoopThread());
  LOG_TRACE << "Connector HandleWrite state=" << StateToString();
  if (state_ == kConnecting) {
    int sockfd = RemoveAndResetChannel();
    int err = sockets::GetSocketError(sockfd);
    if (err) {
      LOG_WARN << "Connector(fd:" << sockfd
               << ") can write, but error SO_ERROR(" << err
               << "):" << strerror_tl(err);
      CloseAndRetry(sockfd);
    } else if (sockets::IsSelfConnect(sockfd)) {
      LOG_WARN << "Connector(fd:" << sockfd
               << ") can write, but is self connect";
      CloseAndRetry(sockfd);
    } else {
      LOG_INFO << "Connector(fd:" << sockfd << ") connect to "
               << server_addr_.ToIpPort() << " success";
      state_ = kConnected;
      if (will_connect_) {
        if (new_connection_callback_) new_connection_callback_(sockfd);
      } else {
        CloseAndRetry(sockfd);
      }
    }
  } else {
    LOG_DEBUG << "Connector(oldfd:xx) can write, but state is not connecting";
  }
}

void Connector::HandleError() {
  XLCOMM_ASSERT(loop_->IsInLoopThread());
  LOG_TRACE << "Connector HandleError state=" << StateToString();
  if (state_ == kConnecting) {
    int sockfd = RemoveAndResetChannel();
    int err = sockets::GetSocketError(sockfd);
    LOG_WARN << "Connector(fd:" << sockfd << ") HandleError, SO_ERROR(" << err
             << "):" << strerror_tl(err);
    CloseAndRetry(sockfd);
  }
}

int Connector::RemoveAndResetChannel() {
  XLCOMM_ASSERT(channel_);
  int sockfd = channel_->fd();
  channel_->DisableAll();
  channel_->Remove();
  // Can't reset channel_ here, because we are inside Channel::HandleEvent
  loop_->QueueInLoop(std::bind(&Connector::ResetChannel, this));  // FIXME
  return sockfd;
}

void Connector::ResetChannel() { channel_.reset(); }

void Connector::CloseAndRetry(int sockfd) {
  sockets::close(sockfd);
  state_ = kDisconnected;
  if (will_connect_) {
    LOG_DEBUG << "Connector(oldfd:" << sockfd << ") will retry connect after "
              << retry_delay_ms_ << " ms...";
    loop_->RunAfter(retry_delay_ms_ / 1000.0,
                    std::bind(&Connector::Connect, this));  // FIXME
    retry_delay_ms_ = std::min(retry_delay_ms_ * 2, kMaxRetryDelayMs);
  } else {
    LOG_INFO << "Connector(oldfd:" << sockfd << ") stop connect to "
             << server_addr_.ToIpPort();
  }
}

const char* Connector::StateToString() const {
  switch (state_) {
    case kDisconnected:
      return "kDisconnected";
    case kConnecting:
      return "kConnecting";
    case kConnected:
      return "kConnected";
    default:
      return "unknown state";
  }
}

}  // namespace net
}  // namespace xlcomm